import { Record } from 'immutable';
import merge from 'lodash/merge';
import memorize from 'lodash-decorators/memoize';
import SCHME_ERROR from '../constant/schema-error';
import { isFunction, isBoolean, isString } from '../utils/is-object';

function assertObject(object) {
  if (object === 'document' || object === 'block') {
    return;
  }
  throw new Error(`node object must be document/block, but got: ${object}`);
}

const DEFAULTS = {
  document: {},
  block: {},
  style: {},
  data: {},
  normalize: (error, node) => node
};

function resolveSchema(plugins = []) {
  const normalizeList = [];
  const schema = {
    document: {},
    block: {},
    style: {},
    data: {},
    normalize: (error, node, editor) => {
      return normalizeList.reduce(
        (normalized, func) => func(error, normalized, editor) || node,
        node
      );
    }
  };
  plugins.forEach(plugin => {
    if (!plugin.schema) return;
    const { document, block, style, data } = plugin.schema;
    merge(schema.document, document);
    merge(schema.block, block);
    merge(schema.style, style);
    merge(schema.data, data);
    if (plugin.schema.normalize) {
      normalizeList.push(plugin.schema.normalize);
    }
  });
  return schema;
}

class Schema extends Record(DEFAULTS) {
  static create(attr = {}) {
    const { plugins, ...rest } = attr;
    let schema = rest;
    if (Array.isArray(plugins)) {
      schema = {
        ...schema,
        ...resolveSchema(plugins)
      };
    }
    return Schema.fromJSON(schema);
  }

  static fromJSON(attr = {}) {
    if (Schema.isSchema(attr)) {
      return attr;
    }
    const { document, block, style, data, normalize } = attr;
    return new Schema({ document, block, style, data, normalize });
  }

  isVoid(node) {
    const rules = this.getNodeRules(node);
    if (rules) {
      return !!rules.isVoid;
    }
  }

  isAtomic(node) {
    const rules = this.getNodeRules(node);
    if (rules) {
      return !!rules.isAtomic;
    }
  }

  isInline(node) {
    const rules = this.getNodeRules(node);
    if (rules) {
      return !!rules.isInline;
    }
  }

  getNodeRules(node) {
    const { type, object } = node;
    assertObject(object);
    const objectRule = this[object];
    if (!type) {
      return {};
    }
    return objectRule[type];
  }

  getGlobalRules() {
    const rules = {
      data: this.data,
      style: this.style,
      normalize: this.normalize
    };
    return rules;
  }

  @memorize
  normalizeNode(node, editor) {
    const globalRules = this.getGlobalRules();
    const rules = this.getNodeRules(node);
    const normalizedNode = [globalRules, rules].reduce(
      (normalizedNode, rules) => normalizeNode(rules, normalizedNode, editor),
      node
    );
    return normalizedNode;
  }
}

function normalizeNode(rules, node, editor) {
  if (!rules) {
    return node;
  }
  if (!isFunction(rules.normalize)) {
    return node;
  }
  const errors = validateNode(node, rules).filter(error => !!error);
  return errors.reduce((normalizedNode, error) => {
    return rules.normalize(error, normalizedNode, editor);
  }, node);
}

function validateNode(node, rules) {
  const errors = [
    ...validateData(rules.data, node),
    ...validateStyle(rules.style, node),
    validateMatch(rules.nodes, node)
  ];
  return errors;
}

function validateMatch(rule, node) {
  if (Array.isArray(rule)) {
    for (const item of rule) {
      const error = validateMatch(item, node);
      if (error) {
        return error;
      }
    }
  }
  if (!rule) {
    return;
  }
  const { match } = rule;
  if (!match) {
    return;
  }
  const { object, type } = match;
  if (object && object !== node.object) {
    return { code: SCHME_ERROR.CHILD_OBJECT_INVALID };
  }
  if (type && type !== node.type) {
    return { code: SCHME_ERROR.CHILD_TYPE_INVALID };
  }
}

function validateData(rule, node) {
  if (isFunction(rule)) {
    const test = rule(node.data, node);
    if (!test) {
      return [
        {
          code: SCHME_ERROR.DATA_INVALID
        }
      ];
    }
    return;
  }
  const errors = [];
  for (const key in rule) {
    const func = rule[key];
    if (isFunction(func)) {
      const test = func(node.data.get(key), node.data, node);
      if (isString(test) && test) {
        errors.push({
          code: SCHME_ERROR.DATA_INVALID,
          key,
          message: test
        });
      } else if (isBoolean(test) && !test) {
        errors.push({
          code: SCHME_ERROR.DATA_INVALID,
          key
        });
      }
    }
  }
  return errors;
}

function validateStyle(rule, node) {
  if (isFunction(rule)) {
    const test = rule(node.data, node);
    if (!test) {
      return [
        {
          code: SCHME_ERROR.STYLE_INVALID
        }
      ];
    }
    return;
  }
  const errors = [];
  for (const key in rule) {
    const func = rule[key];
    if (isFunction(func)) {
      const test = func(node.style.get(key), node.style, node);
      if (isString(test) && test) {
        errors.push({
          code: SCHME_ERROR.STYLE_INVALID,
          key,
          message: test
        });
      } else if (isBoolean(test) && !test) {
        errors.push({
          code: SCHME_ERROR.STYLE_INVALID,
          key
        });
      }
    }
  }
  return errors;
}

Schema.prototype.__schema__ = true;
export default Schema;
